package estargz

import (
	"compress/gzip"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/containerd/containerd/log"
)

const (
	// TOC (Table of Contents) is stored at the end of the layer
	tocFooterSize = 47 // Size of the footer: offset(10) + size(10) + tocMagic(16) + version(10) + newline(1)
	tocMagic      = "stargz.index.v1"
)

type Parser struct{}

type TOC struct {
	Version int        `json:"version"`
	Entries []TOCEntry `json:"entries"`
}

type TOCEntry struct {
	Name        string `json:"name"`
	Type        string `json:"type"`
	Size        int64  `json:"size"`
	ModTime     int64  `json:"modTime"`
	Offset      int64  `json:"offset"`
	ChunkOffset int64  `json:"chunkOffset,omitempty"`
	ChunkSize   int64  `json:"chunkSize,omitempty"`
	ChunkDigest string `json:"chunkDigest,omitempty"`
}

func NewParser() *Parser {
	return &Parser{}
}

// ParseTOC parses the TOC from an estargz layer
func (p *Parser) ParseTOC(ctx context.Context, url string) (*TOC, error) {
	log.G(ctx).WithField("url", url).Debug("parsing estargz TOC")

	// Create HTTP client
	client := &http.Client{}

	// Get layer size first
	headReq, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create HEAD request: %w", err)
	}

	headResp, err := client.Do(headReq)
	if err != nil {
		return nil, fmt.Errorf("failed to get layer size: %w", err)
	}
	headResp.Body.Close()

	layerSize := headResp.ContentLength
	if layerSize < tocFooterSize {
		return nil, fmt.Errorf("layer too small to contain TOC")
	}

	// Read the footer to get TOC offset and size
	footerStart := layerSize - tocFooterSize
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", footerStart, layerSize-1))

	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch footer: %w", err)
	}
	defer resp.Body.Close()

	footer := make([]byte, tocFooterSize)
	if _, err := io.ReadFull(resp.Body, footer); err != nil {
		return nil, fmt.Errorf("failed to read footer: %w", err)
	}

	// Parse footer to get TOC location
	var tocOffset, tocSize int64
	if _, err := fmt.Sscanf(string(footer), "%d,%d,", &tocOffset, &tocSize); err != nil {
		return nil, fmt.Errorf("failed to parse TOC location: %w", err)
	}

	log.G(ctx).WithFields(map[string]interface{}{
		"offset": tocOffset,
		"size":   tocSize,
	}).Debug("found TOC location")

	// Fetch the TOC data
	tocReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create TOC request: %w", err)
	}
	tocReq.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", tocOffset, tocOffset+tocSize-1))

	tocResp, err := client.Do(tocReq)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch TOC: %w", err)
	}
	defer tocResp.Body.Close()

	// Decompress TOC (it's gzipped)
	gzReader, err := gzip.NewReader(tocResp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to create gzip reader: %w", err)
	}
	defer gzReader.Close()

	// Parse TOC JSON
	var toc TOC
	if err := json.NewDecoder(gzReader).Decode(&toc); err != nil {
		return nil, fmt.Errorf("failed to decode TOC: %w", err)
	}

	log.G(ctx).WithField("entries", len(toc.Entries)).Info("parsed TOC successfully")
	return &toc, nil
}

// IsEstargz checks if a layer is in estargz format
func (p *Parser) IsEstargz(ctx context.Context, url string) bool {
	client := &http.Client{}

	headReq, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
	if err != nil {
		return false
	}

	headResp, err := client.Do(headReq)
	if err != nil {
		return false
	}
	headResp.Body.Close()

	layerSize := headResp.ContentLength
	if layerSize < tocFooterSize {
		return false
	}

	// Try to read the footer
	footerStart := layerSize - tocFooterSize
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return false
	}
	req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", footerStart, layerSize-1))

	resp, err := client.Do(req)
	if err != nil {
		return false
	}
	defer resp.Body.Close()

	footer := make([]byte, tocFooterSize)
	if _, err := io.ReadFull(resp.Body, footer); err != nil {
		return false
	}

	// Check for magic string
	return contains(footer, []byte(tocMagic))
}

// GetFileEntry finds a TOC entry by file path
func (p *Parser) GetFileEntry(toc *TOC, path string) *TOCEntry {
	for i := range toc.Entries {
		if toc.Entries[i].Name == path {
			return &toc.Entries[i]
		}
	}
	return nil
}

// Helper function
func contains(data []byte, pattern []byte) bool {
	if len(data) < len(pattern) {
		return false
	}
	for i := 0; i <= len(data)-len(pattern); i++ {
		match := true
		for j := 0; j < len(pattern); j++ {
			if data[i+j] != pattern[j] {
				match = false
				break
			}
		}
		if match {
			return true
		}
	}
	return false
}
